Skip to content

Conversation

@shinwokkang
Copy link
Contributor

@shinwokkang shinwokkang commented Jan 23, 2026

๐Ÿ’ก To Reviewers

  • ํ•ด๋‹น ๋ธŒ๋žœ์น˜์—์„œ ์ƒˆ๋กญ๊ฒŒ ์„ค์น˜ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ๋‹ค๋ฉด ํ•จ๊ป˜ ๋ช…์‹œํ•ด ์ฃผ์„ธ์š”.

  • ์ƒˆ๋กญ๊ฒŒ ์„ค์น˜ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: zustand, js-cookie, react-hot-toast

  • ๋ฆฌ๋ทฐ์–ด๊ฐ€ ์ฝ”๋“œ๋ฅผ ์ดํ•ดํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š” ์ •๋ณด๋‚˜ ์ฐธ๊ณ ์‚ฌํ•ญ์ด ์žˆ๋‹ค๋ฉด ์ž์œ ๋กญ๊ฒŒ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”.

  • ์•„ํ‚คํ…์ฒ˜ ์ฐธ๊ณ ์‚ฌํ•ญ:

    • Service Layer ๋„์ž…: API ํ†ต์‹  ๋กœ์ง์„ src/services๋กœ ๊ฒฉ๋ฆฌํ•˜์—ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ UI๋ฅผ ๋ถ„๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.
      • Custom Hook: ํผ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ useLoginForm์œผ๋กœ ์บก์Аํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
      • ๊ฒฝ๋กœ ์ตœ์ ํ™”: ๋ชจ๋“  ์ž„ํฌํŠธ ๊ฒฝ๋กœ๋Š” @/ ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ”ฅ ์ž‘์—… ๋‚ด์šฉ (๊ฐ€๋Šฅํ•œ ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”)

  • ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ ๋ฐ ๋ฐ˜์‘ํ˜• UI ๊ตฌํ˜„

    • Figma ์ŠคํŽ™ ๊ธฐ๋ฐ˜ Tablet(768px), Mobile(375px) ๋Œ€์‘ ์™„๋ฃŒ.
    • CSS Modules๋ฅผ ์ด์šฉํ•œ ์Šคํƒ€์ผ ์บก์Аํ™”๋กœ ์ „์—ญ ์˜ค์—ผ ์ฐจ๋‹จ.
  • ์ธ์ฆ ์‹œ์Šคํ…œ ์ธํ”„๋ผ ๊ตฌ์ถ•:

    • apiClient ๊ณ ๋„ํ™”: ์ธํ„ฐ์…‰ํ„ฐ(401 ๋Œ€์‘), ํƒ€์ž„์•„์›ƒ, ์—๋Ÿฌ ๋งคํ•‘ ๊ธฐ๋Šฅ ํฌํ•จ.
    • Next.js Middleware: ์ธ์ฆ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋ผ์šฐํŠธ ๋ณดํ˜ธ(๋กœ๊ทธ์ธ ์‹œ /login ์ ‘๊ทผ ์ œํ•œ ๋“ฑ) ์ ์šฉ.
    • Zustand & AuthProvider: ์ƒˆ๋กœ๊ณ ์นจ ์‹œ์—๋„ ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ƒํƒœ ์œ ์ง€(Hydration) ๊ตฌํ˜„.
  • UX ํ”ผ๋“œ๋ฐฑ ์‹œ์Šคํ…œ:

    • alert ์ œ๊ฑฐ ๋ฐ react-hot-toast๋ฅผ ํ†ตํ•œ ๋น„์ฐจ๋‹จํ˜• ์•Œ๋ฆผ ์ ์šฉ.
    • ์ธํ’‹ ํ•˜๋‹จ ์ธ๋ผ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋กœ ์‹ค์‹œ๊ฐ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต.

๐Ÿค” ์ถ”ํ›„ ์ž‘์—… ์˜ˆ์ •

  • ์ถ”๊ฐ€ ๊ตฌํ˜„์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„์ด๋‚˜ ๋‹ค์Œ ์ž‘์—… ๊ณ„ํš์„ ์ž‘์„ฑํ•ด ์ฃผ์„ธ์š”.
    • ํšŒ์›๊ฐ€์ž…(Sign Up) ํŽ˜์ด์ง€ ๋ฐ ์†Œ์…œ ๋กœ๊ทธ์ธ OAuth ์ƒ์„ธ ์—ฐ๋™.
    • ์œ ์ € ํ”„๋กœํ•„ ์ •๋ณด ์กฐํšŒ๋ฅผ ์œ„ํ•œ /auth/me API ์—ฐ๊ฒฐ.

๐Ÿ“ธ ์ž‘์—… ๊ฒฐ๊ณผ (์Šคํฌ๋ฆฐ์ƒท)

  • ์ž‘์—… ๊ฒฐ๊ณผ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ์Šคํฌ๋ฆฐ์ƒท์„ ์ฒจ๋ถ€ํ•ด ์ฃผ์„ธ์š”.

๐Ÿ”— ๊ด€๋ จ ์ด์Šˆ

Summary by CodeRabbit

Release Notes

  • New Features

    • User authentication system with login and multi-step signup
    • Homepage with personalized book recommendations and group listings
    • Book club discovery with search and filtering by category and region
    • Book club creation wizard with profile setup and settings
    • Profile management and user preferences
    • News and book story browsing
    • Subscription and notification features
    • Social login options
  • Bug Fixes & Improvements

    • Enhanced UI/UX with updated typography and color theming
    • Removed deprecated workflow automation
  • Chores

    • Added authentication and state management dependencies

โœ๏ธ Tip: You can customize this high-level summary in your review settings.

@shinwokkang shinwokkang self-assigned this Jan 23, 2026
@shinwokkang shinwokkang added โœจ feat ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ๐Ÿ“ฆ build ๋นŒ๋“œ ์‹œ์Šคํ…œ, ์˜์กด์„ฑ ์„ค์ • labels Jan 23, 2026
@shinwokkang shinwokkang linked an issue Jan 23, 2026 that may be closed by this pull request
4 tasks
@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

๐Ÿ“ Walkthrough

Walkthrough

This pull request introduces a comprehensive authentication and group management system, including login/signup flows with multiple steps, a group/club creation wizard, and extensive new UI components. It also removes GitHub workflow automation files and adds API client integration with Zustand state management.

Changes

Cohort / File(s) Summary
GitHub Workflows & Templates (Deleted)
.github/ISSUE_TEMPLATE/custom.md, .github/PULL_REQUEST_TEMPLATE.md, .github/workflows/deploy.yml, .github/workflows/pr-merge-cleanup.yml, .github/workflows/preview.yml
Removed custom issue/PR templates and three automated workflows (deploy, pr-merge-cleanup, preview), eliminating predefined GitHub automation and issue guidance.
Authentication Core
src/lib/api/client.ts, src/lib/api/endpoints.ts, src/lib/api/ApiError.ts, src/services/authService.ts, src/store/useAuthStore.ts, src/types/auth.ts, src/constants/auth.ts, src/lib/api/errorMapper.ts
Adds HTTP client with 401 handling, login endpoint, error wrapping, auth service (login), Zustand store for user state, and TypeScript types for auth flows and social login options.
Layout & Navigation
src/app/layout.tsx, src/components/layout/Header.tsx, src/components/layout/NavItem.tsx, src/components/auth/AuthProvider.tsx, src/components/common/Toast.tsx
Wraps root layout with AuthProvider and Toaster; adds Header with navigation menu and action icons; NavItem for active nav styling; AuthProvider restores session from cookies on mount.
Page Layouts
src/app/(main)/layout.tsx, src/app/(public)/signup/page.tsx, src/app/(main)/page.tsx, src/app/(main)/ui-test/page.tsx, src/app/groups/layout.tsx, src/app/groups/create/layout.tsx
New layout wrappers and pages for main content, public signup, home, group search, and group creation; signup page implements multi-step flow with state machine.
Join/Signup Flow
src/components/base-ui/Join/JoinButton.tsx, src/components/base-ui/Join/JoinHeader.tsx, src/components/base-ui/Join/JoinInput.tsx, src/components/base-ui/Join/JoinLayout.tsx, src/components/base-ui/Join/steps/*
Reusable join form components (button, header, input with password toggle, layout) and modular signup steps (terms, email verification, password, profile setup, profile image, completion), each with hooks managing local state.
Login Modal
src/components/base-ui/Login/LoginModal.tsx, src/components/base-ui/Login/LoginModal.module.css, src/components/base-ui/Login/useLoginForm.tsx, src/components/base-ui/Login/LoginLogo.tsx, src/components/base-ui/Login/index.ts
Modal dialog for login with email/password, social login buttons, forgot password/signup links; useLoginForm hook handles validation, submission, token storage, and error display; CSS module for styling.
Group Creation Wizard
src/app/groups/create/page.tsx, src/components/base-ui/Group-Create/Chip.tsx, src/components/base-ui/Group-Create/StepDot.tsx
Four-step wizard for club creation: basic info (name, description with duplication check), image upload and visibility, category/participant selection, optional SNS/links; uses chips for multi-select, step indicators for progress tracking.
Group Search & Discovery
src/app/groups/page.tsx, src/app/groups/groupSearchDummy.ts, src/components/base-ui/Group-Search/*
Club search page with filtering by category and region; components for search bar, club list items with apply modals, category tags, club apply form, and user's bookclub sidebar.
Book Story Components
src/components/base-ui/BookStory/bookstory_card.tsx, src/components/base-ui/BookStory/bookstory_choosebook.tsx, src/components/base-ui/BookStory/bookstory_detail.tsx, src/components/base-ui/BookStory/bookstory_text.tsx
Cards and detail sections for displaying book stories with author info, engagement metrics, and editable story text; includes textarea auto-resize and tab-indentation handling.
Profile Components
src/components/base-ui/Profile/mypage_profile.tsx, src/components/base-ui/Profile/others_profile.tsx, src/components/base-ui/Profile/subscribe_element.tsx, src/components/base-ui/Profile/notification_element.tsx
Profile displays for own and others' profiles with follow/subscribe buttons, notification list with type-based messaging (likes, comments), and interactive toggles.
Home Page Components
src/components/base-ui/home/NewsBannerSlider.tsx, src/components/base-ui/home/home_bookclub.tsx, src/components/base-ui/home/list_subscribe.tsx, src/components/base-ui/home/list_subscribe_element.tsx, src/components/base-ui/home/notification_element.tsx
Banner carousel with pagination, collapsible group list with show-all toggle, subscriber list with follow functionality, and notification rows.
Search Components
src/components/base-ui/Search/search_bookresult.tsx, src/components/base-ui/Search/search_recommendbook.tsx
Book search result item with like/action buttons and recommended book cover card with overlay text and interactive like toggle.
Settings & News Components
src/components/base-ui/Settings/setting_news_list.tsx, src/components/base-ui/Settings/setting_report_list.tsx, src/components/base-ui/News/news_list.tsx, src/components/base-ui/News/recommendbook_element.tsx
Reusable list items for news, reports, and book recommendations with metadata, badges, and optional interaction callbacks.
Utility & Configuration
src/utils/groupMapper.ts, src/types/groups/groups.ts, tailwind.config.js, tsconfig.json, src/middleware.ts, src/app/globals.css, package.json
Type definitions and mappers for group categories/participant types; Tailwind config with tablet/desktop breakpoints; middleware for auth-based routing (protects /mypage, gates /login and /signup); CSS with typography utilities and new color tokens; new dependencies (zustand, js-cookie, react-hot-toast).

Sequence Diagrams

sequenceDiagram
    participant User
    participant LoginModal
    participant useLoginForm
    participant authService
    participant apiClient
    participant AuthStore
    participant Router
    
    User->>LoginModal: Enters email & password
    User->>LoginModal: Clicks login button
    LoginModal->>useLoginForm: handleLogin()
    useLoginForm->>useLoginForm: Validate email & password
    useLoginForm->>authService: login(email, password)
    authService->>apiClient: POST /auth/login
    apiClient->>apiClient: Add Authorization header
    apiClient-->>authService: LoginResponse
    useLoginForm->>useLoginForm: Extract accessToken
    useLoginForm->>useLoginForm: Store in cookies (js-cookie)
    useLoginForm->>AuthStore: login({ email })
    useLoginForm->>useLoginForm: Show toast success
    useLoginForm->>Router: navigate("/")
    Router-->>User: Redirect to home
Loading
sequenceDiagram
    participant User
    participant SignupPage
    participant TermsAgreement
    participant EmailVerification
    participant PasswordEntry
    participant ProfileSetup
    participant ProfileImage
    participant SignupComplete
    
    User->>SignupPage: Lands on /signup
    SignupPage->>TermsAgreement: Render Step 1
    User->>TermsAgreement: Accept terms
    TermsAgreement->>SignupPage: onNext() โ†’ step = "email"
    SignupPage->>EmailVerification: Render Step 2
    User->>EmailVerification: Enter email & verify code
    EmailVerification->>SignupPage: onNext() โ†’ step = "password"
    SignupPage->>PasswordEntry: Render Step 3
    User->>PasswordEntry: Enter password
    PasswordEntry->>SignupPage: onNext() โ†’ step = "profile"
    SignupPage->>ProfileSetup: Render Step 4
    User->>ProfileSetup: Enter profile info (nickname, intro, name, phone)
    ProfileSetup->>SignupPage: onNext() โ†’ step = "profile-image"
    SignupPage->>ProfileImage: Render Step 5
    User->>ProfileImage: Upload image & select interests
    ProfileImage->>SignupPage: onNext() โ†’ step = "complete"
    SignupPage->>SignupComplete: Render Step 6
    SignupComplete->>User: Show completion with options (search/create meeting/continue)
Loading
sequenceDiagram
    participant User
    participant CreateClubWizard
    participant Step1
    participant Step2
    participant Step3
    participant Step4
    
    User->>CreateClubWizard: Lands on /groups/create
    CreateClubWizard->>Step1: Render club name & description
    User->>Step1: Enter name, check duplicates, add description
    Step1->>CreateClubWizard: canNext validation passes โ†’ onNext()
    CreateClubWizard->>Step2: Render image upload & visibility
    User->>Step2: Upload profile image, toggle visibility
    Step2->>CreateClubWizard: onNext()
    CreateClubWizard->>Step3: Render category & participant selection
    User->>Step3: Select up to 6 categories, participants, activity area
    Step3->>CreateClubWizard: onNext()
    CreateClubWizard->>Step4: Render optional SNS/links
    User->>Step4: Add social links (dynamic rows)
    Step4->>CreateClubWizard: Complete (submit or finish)
    CreateClubWizard->>User: Club creation complete
Loading

Estimated code review effort

๐ŸŽฏ 5 (Critical) | โฑ๏ธ ~120 minutes

This PR introduces major new subsystems spanning authentication (login/signup with multi-step flows), group/club management (creation wizard, search), comprehensive UI component library, API client with error handling, and state management. The heterogeneous nature of changes across authentication, routing, forms, validation, and UI requires careful review of control flow, validation logic, state transitions, and integration points.

Possibly related PRs

Suggested reviewers

  • hongik-luke
  • psm1st
  • bini0918

Poem

๐Ÿฐ Hop, hop! A login forms with flair,
Auth tokens stored with utmost care,
Sign-ups bloom in steps so neat,
Clubs are born where readers meet!
Component gardens, ripe and grandโ€”
Bound together, oh so planned! ๐ŸŒฑโœจ

๐Ÿšฅ Pre-merge checks | โœ… 2 | โŒ 3
โŒ Failed checks (3 warnings)
Check name Status Explanation Resolution
Linked Issues check โš ๏ธ Warning The PR implements core login API infrastructure (email-based login via authService, apiClient with error handling, login modal UI) but OAuth login API is not implemented, partially meeting issue #36 requirements. Complete the OAuth API integration (Google, Naver, Kakao login flows) referenced in issue #36 or defer OAuth implementation to a follow-up PR with a clear plan.
Out of Scope Changes check โš ๏ธ Warning The PR includes significant out-of-scope changes: removed GitHub Actions workflows (deploy.yml, pr-merge-cleanup.yml, preview.yml), deleted custom issue/PR templates, and added 60+ new UI components and pages unrelated to login API implementation. Separate scope: move removed workflow/template changes and unrelated UI components (signup flow, group pages, news components) into dedicated PRs focused on their respective features.
Docstring Coverage โš ๏ธ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
โœ… Passed checks (2 passed)
Check name Status Explanation
Description Check โœ… Passed Check skipped - CodeRabbitโ€™s high-level summary is enabled.
Title check โœ… Passed The title 'Feat 36 login api' is specific and directly related to the main changes, referring to feature #36 (login API implementation) which aligns with the primary objective.

โœ๏ธ Tip: You can configure your own custom pre-merge checks in the settings.

โœจ Finishing touches
  • ๐Ÿ“ Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

โค๏ธ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @shinwokkang, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

์ด PR์€ ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ๊ตฌ์ถ•ํ•˜๊ณ , ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „๋ฐ˜์ ์ธ UI/UX๋ฅผ ๊ฐœ์„ ํ•˜๋ฉฐ, ๋…์„œ ๋ชจ์ž„ ๊ด€๋ จ ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•˜๋Š” ๋ฐ ์ค‘์ ์„ ๋‘์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋„์ž…๊ณผ Next.js ๋ฏธ๋“ค์›จ์–ด ํ™œ์šฉ์„ ํ†ตํ•ด ์•ˆ์ •์ ์ด๊ณ  ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Highlights

  • ๋กœ๊ทธ์ธ ๋ฐ ํšŒ์›๊ฐ€์ž… ๊ธฐ๋Šฅ ๊ตฌํ˜„: ์‚ฌ์šฉ์ž ์ธ์ฆ์„ ์œ„ํ•œ ๋กœ๊ทธ์ธ API ๋ฐ ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€(์•ฝ๊ด€ ๋™์˜, ์ด๋ฉ”์ผ ์ธ์ฆ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์„ค์ •, ํ”„๋กœํ•„ ์„ค์ •, ํ”„๋กœํ•„ ์ด๋ฏธ์ง€/๊ด€์‹ฌ์‚ฌ ์„ค์ •, ๊ฐ€์ž… ์™„๋ฃŒ)๊ฐ€ ๋‹จ๊ณ„๋ณ„๋กœ ๊ตฌํ˜„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ธ์ฆ ์‹œ์Šคํ…œ ์ธํ”„๋ผ ๊ตฌ์ถ• ๋ฐ ๊ณ ๋„ํ™”: API ํ†ต์‹  ๋กœ์ง์„ src/services๋กœ ๊ฒฉ๋ฆฌํ•˜๊ณ , apiClient์— ์ธํ„ฐ์…‰ํ„ฐ(401 ๋Œ€์‘), ํƒ€์ž„์•„์›ƒ, ์—๋Ÿฌ ๋งคํ•‘ ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค. Next.js Middleware๋ฅผ ํ†ตํ•ด ์ธ์ฆ ์ƒํƒœ์— ๋”ฐ๋ฅธ ๋ผ์šฐํŠธ ๋ณดํ˜ธ๋ฅผ ์ ์šฉํ–ˆ์œผ๋ฉฐ, Zustand์™€ AuthProvider๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ƒํƒœ๋ฅผ ์œ ์ง€(Hydration)ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ƒˆ๋กœ์šด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋„์ž…: zustand (์ƒํƒœ ๊ด€๋ฆฌ), js-cookie (์ฟ ํ‚ค ๊ด€๋ฆฌ), react-hot-toast (๋น„์ฐจ๋‹จํ˜• ์•Œ๋ฆผ) ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ์„ค์น˜ ๋ฐ ์ ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • UI/UX ๊ฐœ์„  ๋ฐ ๋ฐ˜์‘ํ˜• ๋””์ž์ธ ์ ์šฉ: Figma ์ŠคํŽ™ ๊ธฐ๋ฐ˜์œผ๋กœ Tablet(768px) ๋ฐ Mobile(375px)์— ๋Œ€์‘ํ•˜๋Š” ๋ฐ˜์‘ํ˜• ๋กœ๊ทธ์ธ ๋ชจ๋‹ฌ์ด ๊ตฌํ˜„๋˜์—ˆ์œผ๋ฉฐ, CSS Modules๋ฅผ ์ด์šฉํ•œ ์Šคํƒ€์ผ ์บก์Аํ™”๋กœ ์ „์—ญ ์˜ค์—ผ์„ ๋ฐฉ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค. react-hot-toast๋ฅผ ํ†ตํ•œ ๋น„์ฐจ๋‹จํ˜• ์•Œ๋ฆผ๊ณผ ์ธํ’‹ ํ•˜๋‹จ ์ธ๋ผ์ธ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋กœ ์‹ค์‹œ๊ฐ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ๋…์„œ ๋ชจ์ž„ ์ƒ์„ฑ ๋ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€: ๋…์„œ ๋ชจ์ž„ ์ƒ์„ฑ ์›Œํฌํ”Œ๋กœ์šฐ ํŽ˜์ด์ง€์™€ ๋ชจ์ž„ ๊ฒ€์ƒ‰ ๋ฐ ๊ฐ€์ž… ์‹ ์ฒญ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ์ปดํฌ๋„ŒํŠธ๋“ค์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์ „์—ญ ์Šคํƒ€์ผ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ™•์žฅ: globals.css์— ์ƒˆ๋กœ์šด ์ƒ‰์ƒ ๋ณ€์ˆ˜, Pretendard ํฐํŠธ ์„ค์ •, TailwindCSS ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค(headline, subhead, body, caption)๊ฐ€ ์ถ”๊ฐ€๋˜์–ด ๋””์ž์ธ ์‹œ์Šคํ…œ์„ ๊ฐ•ํ™”ํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ๋‹ค์–‘ํ•œ SVG ์•„์ด์ฝ˜ ์ถ”๊ฐ€: public ๋””๋ ‰ํ† ๋ฆฌ์— ํ™”์‚ดํ‘œ, ์ฒดํฌ๋ฐ•์Šค, ๋กœ๊ณ , ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ๋“ฑ ๋‹ค์–‘ํ•œ SVG ์•„์ด์ฝ˜ ํŒŒ์ผ๋“ค์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿง  New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Ignored Files
  • Ignored by pattern: .github/workflows/** (3)
    • .github/workflows/deploy.yml
    • .github/workflows/pr-merge-cleanup.yml
    • .github/workflows/preview.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with ๐Ÿ‘ and ๐Ÿ‘Ž on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. โ†ฉ

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new features related to user authentication, group management, and UI enhancements. It includes the implementation of a login modal, signup process, and various UI components for displaying book stories, news, and group information. The changes also incorporate new libraries for state management (Zustand), cookie handling (js-cookie), and non-blocking notifications (react-hot-toast). I have provided review comments to address potential issues related to date handling and code improvements.

}: Props) {
return (
<div className="flex h-[380px] w-[336px] flex-col overflow-hidden rounded-lg border-2 border-Subbrown-4 bg-White">
{/* ์ƒ๋‹จ ํ”„๋กœํ•„ */}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

high: The createdAt prop is hardcoded with a specific date. This should be dynamically generated or fetched from an API to ensure it reflects the actual creation time of the book story.

fill
className="object-cover"
sizes="32px"
/>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

high: The createdAt prop is hardcoded with a specific date. This should be dynamically generated or fetched from an API to ensure it reflects the actual creation time of the book story.

{timeAgo(createdAt)} ์กฐํšŒ์ˆ˜ {viewCount}
</p>
</div>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

high: The createdAt prop is hardcoded with a specific date. This should be dynamically generated or fetched from an API to ensure it reflects the actual creation time of the book story.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

๐Ÿค– Fix all issues with AI agents
In `@package.json`:
- Line 13: Update the Next.js dependency in package.json by replacing the
current "next": "16.0.1" entry with the patched version that addresses
CVE-2025-66478 (e.g., "next": "16.0.2" or the specific patched release from the
Next.js advisory); after changing the version string for the "next" dependency,
run your package manager (npm/yarn/pnpm) to install and regenerate lockfile to
ensure the patched version is applied.

In `@src/components/base-ui/Group-Search/search_club_apply_modal.tsx`:
- Line 169: The onClick currently calls onSubmit(reason) but onSubmit expects
(club: number, reason: string), so update the handler in
search_club_apply_modal.tsx to pass the club id first and the reason second
(e.g., onSubmit(club, reason) or onSubmit(clubId, reason) depending on the local
prop/name); ensure you reference the component's club identifier (the prop or
state variable used in this component) when invoking onSubmit so the parent
receives the correct club numeric id as the first argument.

In
`@src/components/base-ui/Group-Search/search_clublist/search_clublist_item.tsx`:
- Line 6: The ClubSummary interface currently declares a non-implemented method
reason(clubId: number, reason: string): void which no objects or code use;
remove that method signature from the ClubSummary interface declaration (the
ClubSummary interface in page.tsx) so the type matches the actual dummyClubs and
runtime objects and eliminate the unused contract.
๐ŸŸ  Major comments (26)
src/components/base-ui/News/recommendbook_element.tsx-44-57 (1)

44-57: Add accessible name + pressed state to the like toggle.

Icon-only button is unlabeled for screen readers.

โ™ฟ Suggested fix
         <button
           type="button"
           onClick={(e) => {
             e.stopPropagation();
             onLikeChange(!liked);
           }}
           className="w-[24px] h-[24px] shrink-0"
+          aria-label={liked ? 'Unlike' : 'Like'}
+          aria-pressed={liked}
         >
src/components/base-ui/Search/search_bookresult.tsx-72-101 (1)

72-101: Icon-only buttons need accessible names (and pressed state for like).

Screen readers get unlabeled controls. Add aria-label and aria-pressed for the toggle.

โ™ฟ Suggested fix
           <button
             type="button"
             onClick={(e) => {
               e.stopPropagation();
               onLikeChange(!liked);
             }}
             className="w-[24px] h-[24px] shrink-0"
+            aria-label={liked ? 'Unlike' : 'Like'}
+            aria-pressed={liked}
           >
             <Image
               src={liked ? '/red_heart.svg' : '/gray_heart.svg'}
               alt=""
               width={24}
               height={24}
             />
           </button>

           <button
             type="button"
             onClick={(e) => {
               e.stopPropagation();
               onPencilClick?.();
             }}
             className="
               flex w-[60px] h-[60px] px-[10px] py-[4.167px]
               flex-col justify-center items-center gap-[8.333px] shrink-0
               rounded-full bg-[color:var(--primary_2)]
             "
+            aria-label="Edit"
           >
             <Image src="/pencil_icon.svg" alt="" width={20} height={20} />
           </button>
src/components/base-ui/News/recommendbook_element.tsx-29-33 (1)

29-33: Make the clickable card keyboard-accessible.

A clickable div without role/tabIndex and keyboard handling blocks keyboard-only users.

โ™ฟ Suggested fix
     <div
-      onClick={onCardClick}
+      onClick={onCardClick}
+      onKeyDown={(e) => {
+        if (!onCardClick) return;
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          onCardClick();
+        }
+      }}
+      role={onCardClick ? 'button' : undefined}
+      tabIndex={onCardClick ? 0 : undefined}
       className={`relative flex w-[244px] h-[320px] p-[12px] flex-col justify-end items-start gap-[10px] overflow-hidden ${
         onCardClick ? 'cursor-pointer' : ''
       } ${className}`}
     >
src/components/base-ui/Search/search_recommendbook.tsx-44-57 (1)

44-57: Add accessible name + pressed state to the like toggle.

Icon-only button is unlabeled for screen readers.

โ™ฟ Suggested fix
         <button
           type="button"
           onClick={(e) => {
             e.stopPropagation();
             onLikeChange(!liked);
           }}
           className="w-[24px] h-[24px] shrink-0"
+          aria-label={liked ? 'Unlike' : 'Like'}
+          aria-pressed={liked}
         >
src/components/base-ui/Search/search_bookresult.tsx-37-43 (1)

37-43: Add keyboard access for the clickable card container.

The div is clickable but not keyboard-focusable, blocking keyboard users. Use role/tabIndex and handle Enter/Space when onCardClick is provided.

โ™ฟ Suggested fix
-    <div
-      onClick={onCardClick}
+    <div
+      onClick={onCardClick}
+      onKeyDown={(e) => {
+        if (!onCardClick) return;
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          onCardClick();
+        }
+      }}
+      role={onCardClick ? 'button' : undefined}
+      tabIndex={onCardClick ? 0 : undefined}
       className={[
         'flex w-full max-w-[1040px] p-[20px] justify-center items-start gap-[24px] rounded-[8px] bg-[color:var(--White,`#FFF`)] shadow-[0_2px_4px_rgba(0,0,0,0.05)] border border-[color:var(--Subbrown_4,`#E0E0E0`)]',
         onCardClick ? 'cursor-pointer' : '',
         className,
       ].join(' ')}
     >
src/components/base-ui/Search/search_recommendbook.tsx-29-33 (1)

29-33: Make the clickable card keyboard-accessible.

A clickable div without role/tabIndex and keyboard handling blocks keyboard-only users.

โ™ฟ Suggested fix
     <div
-      onClick={onCardClick}
+      onClick={onCardClick}
+      onKeyDown={(e) => {
+        if (!onCardClick) return;
+        if (e.key === 'Enter' || e.key === ' ') {
+          e.preventDefault();
+          onCardClick();
+        }
+      }}
+      role={onCardClick ? 'button' : undefined}
+      tabIndex={onCardClick ? 0 : undefined}
       className={`relative flex w-[332px] h-[436px] p-[16px] flex-col justify-end items-start gap-[12px] overflow-hidden ${
         onCardClick ? 'cursor-pointer' : ''
       } ${className}`}
     >
src/lib/api/ApiError.ts-1-11 (1)

1-11: Replace any with unknown for type safety.

ESLint flags the use of any on lines 3 and 5. Using unknown provides better type safety while still allowing flexible response data.

๐Ÿ”ง Proposed fix
 export class ApiError extends Error {
   code: string;
-  response?: any;
+  response?: unknown;
 
-  constructor(message: string, code: string = "UNKNOWN_ERROR", response?: any) {
+  constructor(message: string, code: string = "UNKNOWN_ERROR", response?: unknown) {
     super(message);
     this.name = "ApiError";
     this.code = code;
     this.response = response;
   }
 }
src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts-11-24 (1)

11-24: Remove debug artifacts before merging.

The console.log statements in all handlers are debug artifacts. Additionally, the commented-out router.push calls in handleSearchMeeting and handleCreateMeeting suggest incomplete implementation.

๐Ÿงน Proposed cleanup
   const handleSearchMeeting = () => {
-    console.log("Search Meeting clicked");
-    // router.push('/meeting/search');
+    router.push('/meeting/search');
   };
 
   const handleCreateMeeting = () => {
-    console.log("Create Meeting clicked");
-    // router.push('/meeting/create');
+    router.push('/meeting/create');
   };
 
   const handleUseWithoutMeeting = () => {
-    console.log("Use Without Meeting clicked");
     router.push("/");
   };
src/components/base-ui/BookStory/bookstory_choosebook.tsx-1-2 (1)

1-2: Add 'use client' directive for interactive component.

This component has an onClick handler (line 57) which requires client-side JavaScript. Without the 'use client' directive, the button click may not work as expected in Next.js App Router.

๐Ÿ’ก Suggested fix
+'use client';
+
 import React from 'react';
 import Image from 'next/image';
src/components/base-ui/home/home_bookclub.tsx-32-32 (1)

32-32: Use Next.js Image component for consistency and optimization.

Native <img> is used here while the rest of the file uses next/image. Also, the path should have a leading slash for proper public asset resolution.

๐Ÿ’ก Suggested fix
-          <img src="logo2.svg" alt="๋กœ๊ณ " className="mx-auto mb-4 mt-[118px]" />
+          <Image src="/logo2.svg" alt="๋กœ๊ณ " width={100} height={100} className="mx-auto mb-4 mt-[118px]" />
src/components/base-ui/Join/JoinInput.tsx-60-84 (1)

60-84: Associate the label with the input and label the toggle button.

A <span> doesnโ€™t create an accessible label relationship, and the toggle button has no accessible name/state. Use a <label htmlFor> tied to the input and add aria-label/aria-pressed on the toggle. Ensure callers pass id or name when label is provided.

โ™ฟ Proposed fix
-const JoinInput: React.FC<JoinInputProps> = ({
-  label,
-  hideLabel,
-  className,
-  type,
-  ...props
-}) => {
+const JoinInput: React.FC<JoinInputProps> = ({
+  label,
+  hideLabel,
+  className = "",
+  type,
+  ...props
+}) => {
+  const inputId = props.id ?? props.name;
   const [showPassword, setShowPassword] = useState(false);
   const isPasswordType = type === "password";
   const inputType = isPasswordType
     ? showPassword
       ? "text"
       : "password"
     : type;

   return (
     <div className="flex flex-col items-start w-full gap-[12px]">
       {label && (
-        <span
+        <label
+          htmlFor={inputId}
           className={`text-[`#7B6154`] font-sans text-[20px] font-semibold leading-[135%] tracking-[-0.02px] ${
             hideLabel ? "sr-only" : ""
           }`}
         >
           {label}
-        </span>
+        </label>
       )}
       <div className="relative w-full">
         <input
+          id={inputId}
           type={inputType}
           className={`w-full h-[44px] px-[16px] py-[12px] bg-white border rounded-[8px] outline-none ${className} ${
             isPasswordType ? "pr-[40px]" : ""
           }`}
           {...props}
         />
         {isPasswordType && (
           <button
             type="button"
             onClick={() => setShowPassword(!showPassword)}
+            aria-label={showPassword ? "๋น„๋ฐ€๋ฒˆํ˜ธ ์ˆจ๊ธฐ๊ธฐ" : "๋น„๋ฐ€๋ฒˆํ˜ธ ํ‘œ์‹œ"}
+            aria-pressed={showPassword}
             className="absolute top-1/2 right-[12px] transform -translate-y-1/2 text-[`#BBB`] hover:text-[`#8D8D8D`]"
           >
             {showPassword ? <EyeOffIcon /> : <EyeIcon />}
           </button>
         )}
       </div>
     </div>
   );
};
src/app/globals.css-1-1 (1)

1-1: Pin the Pretendard import to a specific version instead of @latest.

Using @latest causes non-deterministic builds and can introduce unexpected breaking changes. Replace with the current stable version (v1.3.9):

`@import` url("https://cdn.jsdelivr.net/gh/orioncactus/[email protected]/dist/web/static/pretendard.min.css");

Consider using .min.css as shown above for better performance.

src/utils/groupMapper.ts-1-1 (1)

1-1: Import types from the canonical source.

Types are imported from @/app/groups/page but should be imported from @/types/groups/groups where Category and ParticipantType are canonically defined. Importing from a page component creates a coupling to that component and may cause circular dependency issues.

๐Ÿ”ง Proposed fix
-import { Category, ParticipantType } from "@/app/groups/page";
+import { Category, ParticipantType } from "@/types/groups/groups";
src/components/base-ui/Group-Search/search_groupsearch.tsx-6-13 (1)

6-13: Duplicate Category type definition.

This Category type is already defined in src/types/groups/groups.ts. Duplicating type definitions can lead to inconsistencies if one is updated but not the other. Import from the canonical source instead.

โ™ป๏ธ Proposed fix
 'use client';

 import Image from 'next/image';
 import { useEffect, useRef, useState } from 'react';
+import { Category } from '@/types/groups/groups';

-export type Category =
-  | '์ „์ฒด'
-  | '๋Œ€ํ•™์ƒ'
-  | '์ง์žฅ์ธ'
-  | '์˜จ๋ผ์ธ'
-  | '๋™์•„๋ฆฌ'
-  | '๋ชจ์ž„'
-  | '๋Œ€๋ฉด';
+// Re-export for consumers that import from this file
+export type { Category } from '@/types/groups/groups';
src/components/base-ui/Join/JoinLayout.tsx-13-15 (1)

13-15: Fixed width will break on mobile/tablet viewports.

The inner container uses a fixed w-[766px] width, which will cause horizontal overflow on screens smaller than 766px. Given the PR objectives mention responsive UI for tablet (768px) and mobile (375px), this needs responsive handling.

๐Ÿ› Suggested fix for responsive layout
-      <div className="flex flex-col items-center w-[766px] px-[56px] py-[99px] gap-[100px] rounded-[8px] bg-White">
+      <div className="flex flex-col items-center w-full max-w-[766px] px-4 sm:px-[56px] py-12 sm:py-[99px] gap-12 sm:gap-[100px] rounded-[8px] bg-White">
src/lib/api/endpoints.ts-1-2 (1)

1-2: Production URL as fallback is risky for development.

If NEXT_PUBLIC_API_URL is not set (e.g., missing .env.local), development environments will silently hit the production API, potentially causing unintended data mutations or auth confusion.

๐Ÿ”ง Safer alternatives

Option 1: Fail explicitly if env var is missing

-export const API_BASE_URL =
-  process.env.NEXT_PUBLIC_API_URL || "https://api.checkmo.co.kr/api";
+const envUrl = process.env.NEXT_PUBLIC_API_URL;
+if (!envUrl) {
+  throw new Error("NEXT_PUBLIC_API_URL environment variable is not set");
+}
+export const API_BASE_URL = envUrl;

Option 2: Use localhost as safe fallback

 export const API_BASE_URL =
-  process.env.NEXT_PUBLIC_API_URL || "https://api.checkmo.co.kr/api";
+  process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001/api";
src/components/base-ui/Join/steps/PasswordEntry/usePasswordEntry.ts-6-15 (1)

6-15: Derive isValid directly instead of using useEffect + useState.

The static analysis correctly flags that calling setState within an effect for derived state causes unnecessary re-renders. Since isValid is purely derived from password and confirmPassword, use useMemo or compute it inline. Also, password.length > 0 is redundant since the regex already requires 6+ characters.

โ™ป๏ธ Proposed fix using useMemo
-import { useState, useEffect } from "react";
+import { useState, useMemo } from "react";

 export const usePasswordEntry = () => {
   const [password, setPassword] = useState("");
   const [confirmPassword, setConfirmPassword] = useState("");
-  const [isValid, setIsValid] = useState(false);

-  useEffect(() => {
-    // 6-12์ž, ์˜๋ฌธ ์ตœ์†Œ 1์ž, ํŠน์ˆ˜๋ฌธ์ž ์ตœ์†Œ 1์ž
-    const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^&*]).{6,12}$/;
-    const isPasswordValid = passwordRegex.test(password);
-    const isMatch = password === confirmPassword;
-
-    setIsValid(isPasswordValid && isMatch && password.length > 0);
-  }, [password, confirmPassword]);
+  // 6-12์ž, ์˜๋ฌธ ์ตœ์†Œ 1์ž, ํŠน์ˆ˜๋ฌธ์ž ์ตœ์†Œ 1์ž
+  const isValid = useMemo(() => {
+    const passwordRegex = /^(?=.*[a-zA-Z])(?=.*[!@#$%^&*]).{6,12}$/;
+    return passwordRegex.test(password) && password === confirmPassword;
+  }, [password, confirmPassword]);
src/app/groups/groupSearchDummy.ts-4-4 (1)

4-4: ClubSummary interface includes a reason method that dummy objects cannot satisfy.

The ClubSummary interface (page.tsx, lines 19-29) declares reason(clubId: number, reason: string): void; as a method. The dummyClubs array in groupSearchDummy.ts defines plain object literals that omit this method, violating the interface contract.

This is a design issueโ€”the reason method is never called on club objects. Instead, reason is handled as a separate callback parameter in event handlers like onSubmitApply(). Methods should not belong in data transfer objects. Remove the reason method from the interface and pass the callback separately where needed, or create a separate handler type for apply actions.

src/components/base-ui/Login/useLoginForm.tsx-78-82 (1)

78-82: Social login path is still a TODO

The PR objectives mention OAuth, but this handler only logs. Please implement or hide/disable the option until itโ€™s ready.

If you want, I can draft the OAuth redirect flow or open a tracking issue.

src/components/base-ui/Join/steps/useEmailVerification.ts-36-40 (1)

36-40: Block verification after the timer expires

handleVerify only checks code length, so a user can still verify after timeout. Guard with timeLeft.

๐Ÿ”ง Suggested fix
  const handleVerify = () => {
-   if (isCodeValid) {
+   if (isCodeValid && timeLeft !== null && timeLeft > 0) {
      setIsVerified(true);
      setShowToast(true);
    }
  };
src/components/base-ui/Join/steps/ProfileSetup/ProfileSetup.tsx-39-45 (1)

39-45: Nickname becomes immutable after duplicate check

disabled={isNicknameChecked} prevents users from correcting typos, while the hookโ€™s reset-on-change can never fire. This blocks signup if they want to edit.

๐Ÿ”ง Suggested fix
-                <JoinInput
-                  value={nickname}
-                  onChange={handleNicknameChange}
-                  disabled={isNicknameChecked}
+                <JoinInput
+                  value={nickname}
+                  onChange={handleNicknameChange}
                   placeholder="๋‹‰๋„ค์ž„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”(์ตœ๋Œ€ 20๊ธ€์ž)"
                   className="border-[`#EAE5E2`] placeholder-[`#BBB`] text-[14px] font-normal"
                 />
src/components/base-ui/Join/steps/useEmailVerification.ts-13-18 (1)

13-18: Reset verification state when the email changes

If the user edits the email after verifying, the existing isVerified/code/timer state carries over and can incorrectly mark a new email as verified.

๐Ÿ”ง Suggested fix
  const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value;
    setEmail(value);
    const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
    setIsEmailValid(emailRegex.test(value));
+   setIsVerified(false);
+   setVerificationCode("");
+   setIsCodeValid(false);
+   setTimeLeft(null);
+   setShowToast(false);
  };
src/components/base-ui/Login/useLoginForm.tsx-47-65 (1)

47-65: Only proceed on true success and avoid leaking auth payloads

The authService.login() returns a LoginResponse object without throwing on failure (indicated by isSuccess: false), so the try-catch does not intercept failed authentications. This means lines 59โ€“64 (state update, toast, and navigation) execute unconditionally even when isSuccess is false. Additionally, line 49 logs the entire response payload, creating potential information leakage. The cookie also lacks an explicit root path.

๐Ÿ”ง Suggested fix
       // Service Layer ํ˜ธ์ถœ
       const data = await authService.login(form);
-
-      console.log("๋กœ๊ทธ์ธ ์„ฑ๊ณต:", data);
-      // 1. Token Storage (Secure Cookie)
-      if (data.isSuccess && data.result?.accessToken) {
-        Cookies.set("accessToken", data.result.accessToken, {
-          secure: true,
-          sameSite: "strict",
-        });
-      }
+      if (!data.isSuccess || !data.result?.accessToken) {
+        throw new ApiError("LOGIN_FAILED", "LOGIN_FAILED", data);
+      }
+
+      // 1. Token Storage (Secure Cookie)
+      Cookies.set("accessToken", data.result.accessToken, {
+        secure: true,
+        sameSite: "strict",
+        path: "/",
+      });
src/app/groups/page.tsx-19-29 (1)

19-29: reason should not be a method signature in a data interface.

The ClubSummary interface defines reason(clubId: number, reason: string): void which is a method signature. This appears to be a mistakeโ€”data interfaces should not contain callback methods. This will cause issues when creating objects that conform to this interface, as they would need to implement a method.

Either remove this line or, if a reason field is needed, define it as a property:

๐Ÿ”ง Proposed fix
 export interface ClubSummary {
-  reason(clubId: number, reason: string): void;
   clubId: number;
   name: string;
   profileImageUrl?: string | null;
   category: number[];
   public: boolean;
   applytype: ApplyType;
   region: string;
   participantTypes: ParticipantType[];
 }
src/lib/api/client.ts-1-1 (1)

1-1: API_BASE_URL is imported but never used.

The base URL is imported but not prepended to requests. Either remove the unused import or prepend it to requestUrl.

๐Ÿ”ง Proposed fix: prepend base URL
-  let requestUrl = url;
+  let requestUrl = `${API_BASE_URL}${url}`;
src/lib/api/client.ts-59-64 (1)

59-64: 401 handler does not interrupt the request flow.

After detecting a 401, the code logs out and shows a toast but then continues to parse and potentially return the response. This can cause callers to receive and process an unauthorized response as valid data, leading to confusing behavior.

๐Ÿ”ง Proposed fix: throw after 401 handling
     // [Resilience] Interceptor: 401 Unauthorized Handling
     if (response.status === 401) {
       console.warn("Session expired. Logging out...");
       useAuthStore.getState().logout();
       toast.error("์„ธ์…˜์ด ๋งŒ๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ๋กœ๊ทธ์ธํ•ด์ฃผ์„ธ์š”.");
-      // ์—ฌ๊ธฐ์„œ throw๋ฅผ ํ•ด์„œ ํ๋ฆ„์„ ๋Š์–ด์ฃผ๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•  ์ˆ˜ ์žˆ์Œ
+      throw new Error("Unauthorized: Session expired");
     }
๐ŸŸก Minor comments (29)
tsconfig.json-35-39 (1)

35-39: Remove stale checkmo/ prefixed paths from include array.

The include array contains checkmo/.next/types/**/*.ts and checkmo/.next/dev/types/**/*.ts (lines 35-36), but the checkmo/ directory does not exist in the repository. These are stale paths and should be removed. The .next/ paths on lines 38-39 are correct for Next.js projects and should be kept (the .next/ directory is generated at build time).

Remove stale paths
   "include": [
     "next-env.d.ts",
     "**/*.ts",
     "**/*.tsx",
-    "checkmo/.next/types/**/*.ts",
-    "checkmo/.next/dev/types/**/*.ts",
     "**/*.mts",
     ".next/types/**/*.ts",
     ".next/dev/types/**/*.ts"
   ],
src/components/base-ui/BookStory/bookstory_text.tsx-29-50 (1)

29-50: Accessibility concern: Tab key interception blocks keyboard navigation.

Preventing the default Tab behavior means keyboard-only users cannot tab out of the textarea to reach other form elements. Consider allowing Tab navigation when no text is selected, or use a modifier key (e.g., Ctrl+Tab or Escape then Tab) for indentation.

๐Ÿ’ก Alternative: Only indent when there's a selection, otherwise allow normal Tab
 const handleDetailKeyDown = useCallback(
   (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
     if (e.key !== 'Tab') return;

     const el = e.currentTarget;
     const start = el.selectionStart ?? 0;
     const end = el.selectionEnd ?? 0;

+    // Allow normal tab navigation when no text is selected
+    if (start === end && !e.shiftKey) return;
+
     e.preventDefault();

     const insert = '  ';
     const next = detail.slice(0, start) + insert + detail.slice(end);
src/components/base-ui/Search/search_bookresult.tsx-66-66 (1)

66-66: Fix likely class typo (flex1 โ†’ flex-1).

This appears to be a utility class typo and can break layout.

๐Ÿฉน Suggested fix
-          <p className="flex1 h-full text-[color:var(--Gray_4,`#8D8D8D`)] body_1_2 line-clamp-6">
+          <p className="flex-1 h-full text-[color:var(--Gray_4,`#8D8D8D`)] body_1_2 line-clamp-6">
src/components/base-ui/Join/steps/SignupComplete/useSignupComplete.ts-6-9 (1)

6-9: Replace hardcoded dummy data with actual user data.

The hook uses hardcoded values instead of retrieving actual user data. This should integrate with the auth store or fetch user profile data after signup completion.

Would you like me to help integrate this with the Zustand auth store to retrieve actual user data?

src/components/common/Toast.tsx-24-31 (1)

24-31: Add ARIA live region attributes for screen-reader announcements.
Right now the toast wonโ€™t be announced reliably by assistive tech.

๐Ÿ› ๏ธ Suggested fix
-    <div
+    <div
+      role="status"
+      aria-live="polite"
+      aria-atomic="true"
       className={`absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 inline-flex justify-center items-center h-[88px] pl-[138px] pr-[137px] bg-[`#31111D99`] rounded-[24px] backdrop-blur-[1px] transition-opacity duration-300 ${
         isVisible ? "opacity-100" : "opacity-0"
       }`}
     >
src/components/base-ui/Profile/others_profile.tsx-88-100 (1)

88-100: Expose toggle state via aria-pressed.
This is a toggle button, so assistive tech should get the pressed state.

๐Ÿ› ๏ธ Suggested fix
-        <button
+        <button
           type="button"
           onClick={() => onToggleSubscribe(!isSubscribed)}
+          aria-pressed={isSubscribed}
           className={[
             'flex w-[532px] h-[48px] px-[16px] py-[12px] justify-center items-center gap-[10px] rounded-[8px]',
             'subhead_4_1 whitespace-nowrap',
             isSubscribed
               ? 'bg-[color:var(--Subbrown_4,`#EAE5E2`)] text-[color:var(--primary_3,`#5E4A40`)]'
               : 'bg-[color:var(--Primary_1,`#7B6154`)] text-[color:var(--White,`#FFF`)]',
           ].join(' ')}
         >
src/components/base-ui/Join/steps/SignupComplete/SignupComplete.tsx-35-41 (1)

35-41: Use a descriptive alt for the profile image.
Helps accessibility and aligns with user-specific content.

๐Ÿ› ๏ธ Suggested fix
-            <Image
-              src={profileImage}
-              alt="Profile"
+            <Image
+              src={profileImage}
+              alt={`${nickname} ํ”„๋กœํ•„`}
               width={138}
               height={138}
               className="object-cover w-full h-full"
             />
src/components/base-ui/home/list_subscribe.tsx-25-25 (1)

25-25: Remove console.log before production.

Debug logging should not remain in production code. Replace with actual subscription logic or a no-op placeholder.

๐Ÿ’ก Suggested fix
-            onSubscribeClick={() => console.log('subscribe', u.id)}
+            onSubscribeClick={() => {
+              // TODO: Implement subscription logic
+            }}
src/components/base-ui/home/home_bookclub.tsx-14-19 (1)

14-19: Inconsistent threshold vs. preview count.

The component triggers collapse mode when count >= 5, but then displays 6 items when collapsed. This creates an edge case where having exactly 5 groups would show 5 items with a "์ „์ฒด๋ณด๊ธฐ" toggle that does nothing meaningful.

Consider aligning these values:

๐Ÿ’ก Suggested fix
-  const isMany = count >= 5;
+  const isMany = count > 6;

   const [open, setOpen] = useState(false);

   // ์ ‘ํž˜: 6๊ฐœ๋งŒ / ํŽผ์นจ: ์ „์ฒด
   const displayGroups = isMany && !open ? groups.slice(0, 6) : groups;
src/components/base-ui/home/list_subscribe_element.tsx-24-32 (1)

24-32: Mismatched sizes prop value.

The Image container is 32x32px but sizes="42px" is specified. This should match the actual rendered size for optimal image loading.

๐Ÿ’ก Suggested fix
         <Image
           src={profileSrc}
           alt={`${name} profile`}
           fill
           className="object-cover"
-          sizes="42px"
+          sizes="32px"
           priority={false}
         />
src/components/base-ui/BookStory/bookstory_card.tsx-18-30 (1)

18-30: Add validation for invalid date strings.

The timeAgo function doesn't handle invalid ISO strings, which would cause new Date(iso) to return Invalid Date and result in NaN-based calculations returning unexpected output like "NaN์ผ ์ „".

๐Ÿ’ก Suggested defensive fix
 function timeAgo(iso: string) {
+  const date = new Date(iso);
+  if (isNaN(date.getTime())) return '';
-  const diff = Date.now() - new Date(iso).getTime();
+  const diff = Date.now() - date.getTime();
   const minutes = Math.floor(diff / 60000);
src/components/base-ui/Group-Search/search_mybookclub.tsx-69-85 (1)

69-85: Conflicting CSS classes: grid and flex-col.

Line 71 combines grid grid-cols-1 with flex-col. These are mutually exclusive layout modesโ€”flex-col has no effect when display: grid is applied. Remove flex-col.

๐Ÿ”ง Proposed fix
             className={[
-              "grid grid-cols-1 t:grid-cols-2 d:grid-cols-1 flex-col gap-2",
+              "grid grid-cols-1 t:grid-cols-2 d:grid-cols-1 gap-2",
               open && showToggle ? "overflow-y-auto pr-1" : "",
             ].join(" ")}
src/components/base-ui/Group-Search/search_mybookclub.tsx-62-65 (1)

62-65: Empty Tailwind class h-[] appears to be incomplete.

The h-[] class on line 62 is an empty arbitrary value that has no effect. This looks like incomplete code or a typo. Either remove it or specify an intended height value.

๐Ÿ”ง Proposed fix
-        <div className="h-[] flex items-center justify-center py-4 t:py-10 d:py-20">
+        <div className="flex items-center justify-center py-4 t:py-10 d:py-20">
src/components/auth/AuthProvider.tsx-10-21 (1)

10-21: Token hydration sets incomplete user state without validation.

The current implementation trusts the cookie token existence without validating it server-side. This means:

  1. An expired or tampered token will set isLoggedIn: true until the first API call fails
  2. user.email is empty, which may cause issues if other components expect it when isLoggedIn is true

The TODO comment indicates this is intentional for now, but consider prioritizing the /api/auth/me call to validate the token and fetch complete user data on hydration.

Would you like me to help implement the token validation flow using /api/auth/me?

src/components/base-ui/Login/LoginModal.tsx-58-69 (1)

58-69: Email input should use type="email".

Using type="text" loses browser validation, mobile keyboard optimization, and autocomplete capabilities for email fields.

Proposed fix
               <input
                 name="email"
-                type="text"
+                type="email"
                 value={form.email}
                 onChange={handleChange}
                 placeholder="์ด๋ฉ”์ผ"
src/app/groups/groupSearchDummy.ts-7-17 (1)

7-17: Duplicate IDs in dummy data will cause React key collisions.

The mydummyGroup array contains duplicate id values ('1', '2', '3', '4' each appear twice). When this data is rendered with id as a React key, you'll get key collision warnings and potential rendering bugs.

Proposed fix
 export const mydummyGroup: GroupSummary[] = [
   { id: '1', name: '๋ชจ์ž„1' },
   { id: '2', name: '๋ชจ์ž„2' },
   { id: '3', name: '๋ชจ์ž„3' },
   { id: '4', name: '๋ชจ์ž„4' },
-  { id: '1', name: '๋ชจ์ž„11241' },
-  { id: '2', name: '๋ชจ์ž„51212' },
-  { id: '3', name: '๋ชจ์ž„125153' },
-  { id: '4', name: '๋ชจ์ž„12512514' },
+  { id: '5', name: '๋ชจ์ž„11241' },
+  { id: '6', name: '๋ชจ์ž„51212' },
+  { id: '7', name: '๋ชจ์ž„125153' },
+  { id: '8', name: '๋ชจ์ž„12512514' },
 ];
src/components/base-ui/home/NewsBannerSlider.tsx-17-17 (1)

17-17: Fixed dimensions break responsiveness.

The container uses hardcoded h-[424px] w-[1040px], which won't adapt to tablet (768px) or mobile (375px) breakpoints mentioned in the PR objectives.

Suggested responsive approach
-    <div className="relative h-[424px] w-[1040px] overflow-hidden rounded-[10px]">
+    <div className="relative aspect-[1040/424] w-full max-w-[1040px] overflow-hidden rounded-[10px]">
src/components/base-ui/Login/LoginModal.tsx-136-141 (1)

136-141: Same accessibility issue: "ํšŒ์›๊ฐ€์ž…ํ•˜๋Ÿฌ๊ฐ€๊ธฐ" should be a button.

The signup link in the footer has the same accessibility problem as the find account links.

Proposed fix
           <p className={styles.footerText}>
             ์•„์ง ํšŒ์›์ด ์•„๋‹ˆ์‹ ๊ฐ€์š”?{" "}
-            <span className={styles.footerLink} onClick={onSignUp}>
+            <button type="button" className={styles.footerLink} onClick={onSignUp}>
               ํšŒ์›๊ฐ€์ž…ํ•˜๋Ÿฌ๊ฐ€๊ธฐ
-            </span>
+            </button>
           </p>
src/components/base-ui/Login/LoginModal.tsx-91-99 (1)

91-99: Interactive <span> elements are not keyboard accessible.

Using <span> with onClick for "์•„์ด๋”” ์ฐพ๊ธฐ" and "๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ" lacks keyboard support (no focus, no Enter/Space activation). Use <button> elements instead.

Proposed fix
               <div className={styles.findAccount}>
-                <span className={styles.link} onClick={onFindAccount}>
+                <button type="button" className={styles.link} onClick={onFindAccount}>
                   ์•„์ด๋”” ์ฐพ๊ธฐ
-                </span>
+                </button>
                 <span className={styles.divider}>|</span>
-                <span className={styles.link} onClick={onFindAccount}>
+                <button type="button" className={styles.link} onClick={onFindAccount}>
                   ๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ
-                </span>
+                </button>
               </div>
src/app/(main)/page.tsx-23-25 (1)

23-25: LoginModal is missing onFindAccount and onSignUp handlers.

According to the LoginModal component signature, it accepts onFindAccount and onSignUp optional props. Currently, clicking "์•„์ด๋”” ์ฐพ๊ธฐ", "๋น„๋ฐ€๋ฒˆํ˜ธ ์ฐพ๊ธฐ", or "ํšŒ์›๊ฐ€์ž…ํ•˜๋Ÿฌ๊ฐ€๊ธฐ" links will have no effect since these handlers aren't provided.

Consider wiring up placeholder handlers or disabling those UI elements until the functionality is implemented.

src/components/base-ui/Join/JoinButton.tsx-18-23 (1)

18-23: Secondary variant lacks disabled styling.

The primary variant has explicit disabled styling, but secondary does not. A disabled secondary button will remain visually unchanged, which may confuse users.

๐Ÿ› Proposed fix
   const variants = {
     primary: disabled
       ? "bg-[`#DADADA`] text-[`#8D8D8D`] cursor-not-allowed"
       : "bg-[`#7B6154`] text-[`#FFF`]",
-    secondary: "bg-[`#EAE5E2`] text-[`#5E4A40`] border border-[`#D2C5B6`]",
+    secondary: disabled
+      ? "bg-[`#F5F5F5`] text-[`#BBBBBB`] border border-[`#E0E0E0`] cursor-not-allowed"
+      : "bg-[`#EAE5E2`] text-[`#5E4A40`] border border-[`#D2C5B6`]",
   };
src/components/base-ui/Group-Search/search_clublist/search_club_category_tags.tsx-50-56 (1)

50-56: Typo: item-center should be items-center.

Line 51 has a typo in the Tailwind class name. item-center is not a valid Tailwind utility; it should be items-center.

๐Ÿ› Proposed fix
           <span
             key={n}
             className={[
-              'h-[21px] my-auto py-[1px] inline-flex item-center justify-center body_1_2',
+              'h-[21px] my-auto py-[1px] inline-flex items-center justify-center body_1_2',
               'rounded-[8px] text-White',
               short ? 'w-[44px]' : 'px-2',
               getBgByCategory(n),
               className,
             ].join(' ')}
           >
src/components/base-ui/Join/steps/ProfileImage/InterestCategorySelector.tsx-29-33 (1)

29-33: Add explicit button type to prevent future form submission issues

The button currently lacks an explicit type attribute. While the component is not currently rendered inside a <form>, adding type="button" is a best practice to prevent accidental form submission if the component is ever used in a form context in the future.

๐Ÿ”ง Suggested fix
             <button
               key={category}
+              type="button"
+              aria-pressed={isSelected}
               onClick={() => onToggle(category)}
               className={`w-[122px] h-[44px] flex justify-center items-center rounded-[400px] text-[14px] leading-[145%] tracking-[-0.014px] transition-colors ${
src/components/base-ui/Join/steps/TermsItem.tsx-33-41 (1)

33-41: Make checkbox icons decorative for screen readers

The label already conveys state; these images should be marked as decorative using an empty alt="" attribute to prevent screen readers from announcing them.

๐Ÿ”ง Suggested fix
-          <Image
-            src="/CheckBox_No.svg"
-            alt="Unchecked"
-            width={24}
-            height={24}
-          />
+          <Image
+            src="/CheckBox_No.svg"
+            alt=""
+            width={24}
+            height={24}
+          />
...
-          <Image src="/CheckBox_Yes.svg" alt="Checked" width={24} height={24} />
+          <Image
+            src="/CheckBox_Yes.svg"
+            alt=""
+            width={24}
+            height={24}
+          />
src/app/groups/create/page.tsx-411-416 (1)

411-416: Missing maxLength attribute for activity area input.

Similar to the description textarea, this input's placeholder says "40์ž ์ œํ•œ" but no maxLength={40} is set.

๐Ÿ”ง Proposed fix
                     <input
                       value={activityArea}
                       onChange={(e) => setActivityArea(e.target.value)}
                       placeholder="ํ™œ๋™ ์ง€์—ญ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š” (40์ž ์ œํ•œ)"
+                      maxLength={40}
                       className="..."
                     />
src/components/base-ui/Join/steps/EmailVerification/EmailVerification.tsx-30-30 (1)

30-30: Fixed width may break on mobile viewports.

The container uses w-[766px] which exceeds mobile viewport width (375px per PR objectives). Consider using responsive classes like w-full max-w-[766px] or adding breakpoint variants.

๐Ÿ”ง Suggested responsive fix
-    <div className="relative flex flex-col items-center w-[766px] px-[56px] py-[99px] bg-white rounded-[8px]">
+    <div className="relative flex flex-col items-center w-full max-w-[766px] px-4 sm:px-[56px] py-10 sm:py-[99px] bg-white rounded-[8px]">
src/app/groups/create/page.tsx-195-216 (1)

195-216: Missing maxLength attribute despite placeholder indicating limit.

The placeholder text states "500์ž ์ œํ•œ" but there's no maxLength={500} attribute to enforce the limit. Users can currently enter unlimited text.

๐Ÿ”ง Proposed fix
                     <textarea
                       value={clubDescription}
                       onChange={(e) => {
                         setClubDescription(e.target.value);
                         autoResize(e.currentTarget);
                       }}
                       onInput={(e) => autoResize(e.currentTarget)}
                       placeholder="์ž์œ ๋กญ๊ฒŒ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”! (500์ž ์ œํ•œ)"
+                      maxLength={500}
                       className="..."
                     />
src/app/groups/create/page.tsx-473-539 (1)

473-539: Avoid using array index as key for dynamic lists.

Using key={idx} for the links list can cause incorrect React reconciliation when rows are removed. If the user deletes the first row, the second row's state may not update correctly.

๐Ÿ”ง Proposed fix: use a stable unique ID
-type SnsLink = { label: string; url: string };
+type SnsLink = { id: number; label: string; url: string };

+const nextLinkId = useRef(1);

-const [links, setLinks] = useState<SnsLink[]>([{ label: "", url: "" }]);
+const [links, setLinks] = useState<SnsLink[]>([{ id: 0, label: "", url: "" }]);

 const addLinkRow = () => {
-  setLinks((prev) => [...prev, { label: "", url: "" }]);
+  setLinks((prev) => [...prev, { id: nextLinkId.current++, label: "", url: "" }]);
 };

// Then in JSX:
-{links.map((it, idx) => (
-  <div key={idx} ...>
+{links.map((it) => (
+  <div key={it.id} ...>
src/components/base-ui/Join/steps/ProfileImage/useProfileImage.ts-49-56 (1)

49-56: Memory leak: old blob URL not revoked when replaced.

The cleanup function only runs on unmount (or when profileImage changes), but at that point it revokes the new value. When a user uploads a second image, the previous blob URL is never revoked.

๐Ÿ”ง Proposed fix using a ref to track the previous blob URL
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";

...

+  const prevBlobRef = useRef<string | null>(null);
+
   const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0];
     if (file) {
       const imageUrl = URL.createObjectURL(file);
       setProfileImage(imageUrl);
     }
   };

   // ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ cleanup
   useEffect(() => {
+    // Revoke previous blob URL when profileImage changes
+    if (prevBlobRef.current && prevBlobRef.current.startsWith("blob:")) {
+      URL.revokeObjectURL(prevBlobRef.current);
+    }
+    prevBlobRef.current = profileImage;
+
     return () => {
       if (profileImage && profileImage.startsWith("blob:")) {
         URL.revokeObjectURL(profileImage);
       }
     };
   }, [profileImage]);

@shinwokkang shinwokkang changed the base branch from main to dev January 23, 2026 11:57
@shinwokkang shinwokkang merged commit 2cd45c9 into dev Jan 23, 2026
1 check passed
@shinwokkang shinwokkang deleted the feat-36-login-api branch January 23, 2026 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

๐Ÿ“ฆ build ๋นŒ๋“œ ์‹œ์Šคํ…œ, ์˜์กด์„ฑ ์„ค์ • โœจ feat ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] ๋กœ๊ทธ์ธ API ๊ตฌํ˜„

2 participants